package com.thiswhyme.common.utils;

import com.thiswhyme.common.expection.BusinessException;
import com.thiswhyme.common.response.ResponseCode;
import com.thiswhyme.common.utils.json.fastjson.FastJsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.http.Consts;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * httpClient工具类
 *
 * @author: thiswhyme
 * @date: 2021/8/17
 */
@Slf4j
public class HttpClientUtils {

	/**
	 * 从连接池获取连接超时时间（ms）
	 */
	private static final int CONNECT_REQUEST_TIMEOUT = 5 * 1000;
	/**
	 * 连接超时时间（ms）
	 */
	private static final int CONNECT_TIMEOUT = 5 * 1000;
	/**
	 * 获取数据的超时时间（即响应时间，ms）
	 */
	private static final int SOCKET_TIMEOUT = 15 * 1000;
	/**
	 * 最大连接数
	 */
	private static final int MAX_CONN = 200;
	/**
	 * 每个路由最大连接数
	 */
	private static final int MAX_PRE_ROUTE = 20;
	/**
	 * 定时关闭闲置时间超过该值的连接（s）
	 */
	private static final int IDLE_TIMEOUT = 10;

    private static PoolingHttpClientConnectionManager connectionManager;
	private static CloseableHttpClient httpClient;

    static {
        // 设置连接池
    	Registry<ConnectionSocketFactory> socketFactoryRegistry = 
    			RegistryBuilder.<ConnectionSocketFactory>create()
    			.register("http", PlainConnectionSocketFactory.INSTANCE)
    			.register("https", getSocketFactory(null))
    			.build();
		connectionManager.setMaxTotal(MAX_CONN);
		connectionManager.setDefaultMaxPerRoute(MAX_PRE_ROUTE);
		connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

		// 创建 httpClient
		httpClient = HttpClients
				.custom()
				.setConnectionManager(connectionManager)
				.setDefaultRequestConfig(RequestConfig.custom()
						.setConnectionRequestTimeout(CONNECT_REQUEST_TIMEOUT)
						.setConnectTimeout(CONNECT_TIMEOUT)
						.setSocketTimeout(SOCKET_TIMEOUT).build())
				.evictExpiredConnections()
				.evictIdleConnections(IDLE_TIMEOUT, TimeUnit.SECONDS)
				.build();

		// 关闭连接池钩子
		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
			try {
				httpClient.close();
				connectionManager.close();
			} catch (IOException e) {
				log.error("连接池关闭失败", e);
			}
		}));
    }

	/**
	 * 为HTTPS请求 创建SSLSocketFactory实例
	 *
	 * @param sslContext 自定义 SSLContext，非必填
	 * @return
	 */
	private static SSLConnectionSocketFactory getSocketFactory(SSLContext sslContext) {
		// 无需验证时，生成 SSLContext，绕过 ssl 验证
		if (null == sslContext) {
			try {
				sslContext = SSLContexts.custom().loadTrustMaterial(null, (chain, authType) -> true).build();
			} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
				log.error("构建 SSLConnectionSocketFactory 失败", e);
				throw new BusinessException(ResponseCode.SYSTEM_EXCEPTION);
			}

		}
		return new SSLConnectionSocketFactory(sslContext);
	}

	/**
	 * get 请求
	 *
	 * @param url 请求路径
	 * @return 响应文本
	 */
    public static String httpGet(String url) {
    	return HttpClientUtils.httpGet(url, null);
    }

	/**
	 * get 请求
	 *
	 * @param url 请求路径
	 * @param params 请求参数
	 * @return 响应文本
	 */
	public static String httpGet(String url, Map<String, String> params) {
    	return HttpClientUtils.httpGet(url, params, null, null);
	}

	/**
	 * get 请求
	 *
	 * @param url 请求路径
	 * @param params 请求参数
	 * @param headers 请求头
	 * @param charset 字符集   
	 * @return 响应文本
	 */
	public static String httpGet(String url, Map<String, String> params, Map<String, String> headers, String charset) {
		StopWatch watch = new StopWatch();
		CloseableHttpResponse resp = null;
		String respondBody;
		try {
			HttpGet httpGet = new HttpGet(buildUri(params, url));
			setHeaders(headers, httpGet);
			watch.start();
			resp = httpClient.execute(httpGet);
			watch.stop();
			respondBody = EntityUtils.toString(resp.getEntity(),
					StringUtils.isBlank(charset) ? Consts.UTF_8 : Charset.forName(charset));
		} catch (Exception e) {
			log.error("调用【GET:{}】失败，请求参数：{}", url, FastJsonUtils.obj2String(params), e);
			throw new BusinessException(ResponseCode.RPC_CALL_ERROR);
		} finally {
			release(resp);
		}
		log.debug("调用【GET:{}】成功，耗时：{}，响应参数：{}", url, watch.getTime(), respondBody);
		return respondBody;
	}
	
	/**
	 * post 请求
	 *
	 * @param url 请求路径
	 * @param json json 类型参数
	 * @return 响应文本
	 */
	public static String httpPost(String url, String json) {
		return HttpClientUtils.httpPost(url, json, null, null);
	}

	/**
	 * post 请求
	 * 
	 * @param url 请求路径
	 * @param json json 类型参数
	 * @param headers 请求头 
	 * @return 响应文本
	 */
	public static String httpPost(String url, String json, Map<String, String> headers) {
		return HttpClientUtils.httpPost(url, json, headers, null);
	}

	/**
	 * post 请求
	 * 
	 * @param url 请求路径
	 * @param json json 类型参数
	 * @param headers 请求头
	 * @param charset 字符集
	 * @return 响应文本
	 */
	public static String httpPost(String url, String json, Map<String, String> headers, String charset) {
		StopWatch watch = new StopWatch();
		CloseableHttpResponse resp = null;
		String respondBody;
		try {
			Charset cs = StringUtils.isBlank(charset) ? Consts.UTF_8 : Charset.forName(charset);
			HttpPost httpPost = new HttpPost(url);
			httpPost.setEntity(new StringEntity(json, cs));
			setHeaders(headers, httpPost);
			watch.start();
			resp = httpClient.execute(httpPost);
			watch.stop();
			respondBody = EntityUtils.toString(resp.getEntity(), cs);
		} catch (Exception e) {
			log.error("调用【POST:{}】失败，请求参数：{}", url, json, e);
			throw new BusinessException(ResponseCode.RPC_CALL_ERROR);
		} finally {
			release(resp);
		}
		log.debug("调用【POST:{}】成功，耗时：{}，响应参数：{}", url, watch.getTime(), respondBody);
		return respondBody;
	}

	/**
	 * post 请求
	 * 
	 * @param url 请求路径
	 * @param params 请求参数
	 * @return 响应文本
	 */
	public static String httpPost(String url, Map<String, String> params) {
		return HttpClientUtils.httpPost(url, params, null, null);
	}

	/**
	 * post 请求
	 *
	 * @param url 请求路径
	 * @param params 请求参数
	 * @param headers 请求头 
	 * @return 响应文本
	 */
	public static String httpPost(String url, Map<String, String> params, Map<String, String> headers) {
		return HttpClientUtils.httpPost(url, params, headers, null);
	}

	/**
	 * post 请求
	 * @param url 请求路径
	 * @param params 请求参数
	 * @param headers 请求头
	 * @param charset 字符集
	 * @return 响应文本
	 */
	public static String httpPost(String url, Map<String, String> params, Map<String, String> headers, String charset) {
		StopWatch watch = new StopWatch();
		CloseableHttpResponse resp = null;
		String respondBody;
		try {
			Charset cs = StringUtils.isBlank(charset) ? Consts.UTF_8 : Charset.forName(charset);
			HttpPost httpPost = new HttpPost(url);
			setParams(params, cs, httpPost);
			setHeaders(headers, httpPost);
			watch.start();
			resp = httpClient.execute(httpPost);
			watch.stop();
			respondBody = EntityUtils.toString(resp.getEntity(), cs);
		} catch (Exception e) {
			log.error("调用【POST:{}】失败，请求参数：{}", url, FastJsonUtils.obj2String(params), e);
			throw new BusinessException(ResponseCode.RPC_CALL_ERROR);
		} finally {
			release(resp);
		}
		log.debug("调用【POST:{}】成功，耗时：{}，响应参数：{}", url, watch.getTime(), respondBody);
		return respondBody;
	}

	/**
	 * 构建 RequestConfig
	 *
	 * @param connectTimeout 连接超时时间
	 * @param connectionRequestTimeout 从线程池中获取线程超时时间
	 * @param socketTimeout 数据获取超时时间
	 * @return
	 */
	public static RequestConfig buildSimpleRequestConfig(Integer connectTimeout, Integer connectionRequestTimeout,
												   Integer socketTimeout) {
		return RequestConfig.custom().setConnectTimeout(connectTimeout)
				.setConnectionRequestTimeout(connectionRequestTimeout)
				.setSocketTimeout(socketTimeout)
				.build();
	}

	/**
	 * 设置请求头
	 *
	 * @param headers
	 * @param httpMethod
	 */
	private static void setHeaders(Map<String, String> headers, HttpRequestBase httpMethod) {
		if (headers != null && !headers.isEmpty()) {
			for (Map.Entry<String, String> entry : headers.entrySet()) {
				Optional.ofNullable(entry.getValue())
						.ifPresent(value -> httpMethod.setHeader(entry.getKey(), value));
			}
		}
	}

	/**
	 * 将参数拼装到 url
	 *
	 * @param params
	 * @param url
	 * @return
	 */
	private static URI buildUri(Map<String, String> params, String url) {
		URI finalUrl;
		try {
			URIBuilder uriBuilder = new URIBuilder(url);
			if (params != null && !params.isEmpty()) {
				Set<Map.Entry<String, String>> entrySet = params.entrySet();
				for (Map.Entry<String, String> entry : entrySet) {
					uriBuilder.setParameter(entry.getKey(), entry.getValue());
				}
			}
			finalUrl = uriBuilder.build();
		} catch (URISyntaxException e) {
			log.error("请求【{}】参数拼接失败", url, e);
			throw new BusinessException("拼接参数失败");
		}
		return finalUrl;
	}

	/**
	 * 设置参数
	 *
	 * @param params
	 * @param cs
	 * @param httpMethod
	 */
	private static void setParams(Map<String, String> params, Charset cs, HttpEntityEnclosingRequestBase httpMethod) {
		if (params != null && !params.isEmpty()) {
			List<NameValuePair> pairs = new ArrayList<>();
			for (Map.Entry<String, String> entry : params.entrySet()) {
				if (!StringUtils.isBlank(entry.getKey()) && !StringUtils.isBlank(entry.getValue())) {
					pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
				}
			}
			httpMethod.setEntity(new UrlEncodedFormEntity(pairs, cs));
		}
	}

	/**
	 * 释放资源
	 *
	 * @param httpResponse
	 */
	private static void release(CloseableHttpResponse httpResponse) {
		try {
			if (httpResponse != null) {
				EntityUtils.consume(httpResponse.getEntity());
				httpResponse.close();
			}
		} catch (IOException e) {
			log.error("关闭 HTTP 响应时抛出异常，需要关注", e);
		}
	}
}
